﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;

namespace Html.Utilities
{
    /// <summary>
    /// A helper class to deal with <see cref="HtmlDocument"/>.
    /// </summary>
    internal static class HtmlHelper
    {
        /// <summary>
        /// Creates a "Div" element and append a copy of all the given node to it.
        /// </summary>
        /// <param name="nodes">The nodes that will be copied to the div.</param>
        /// <returns>The "Div".</returns>
        public static HtmlNode GroupCopyOfNodes(IEnumerable<HtmlNode> nodes)
        {
            if (nodes != null)
            {
                HtmlNode groupingDiv = null;
                foreach (HtmlNode node in nodes)
                {
                    if (groupingDiv == null)
                    {
                        groupingDiv = node.OwnerDocument.CreateElement("div");
                    }
                    groupingDiv.AppendChild(node.CloneNode(true));
                }
                return groupingDiv;
            }
            return null;
        }

        /// <summary>
        /// Gets the <see cref="HtmlNode.OwnerDocument"/> of the given nodes with only 
        /// the content between the <see cref="topNode"/> and the <see cref="bottomNode"/>.<br/>
        /// The <see cref="topNode"/> most be before the <see cref="bottomNode"/>.<br/>
        /// If The <see cref="bottomNode"/> is null the method will get the part of the html 
        /// between the <see cref="topNode"/> and the end of the document.
        /// </summary>
        /// <param name="topNode">
        /// You will get the content <b>after</b> this node (not including the node).
        /// </param>
        /// <param name="bottomNode">
        /// You will get the content <b>before</b> this node (not including the node).<br/>
        /// If this is null, you'll get the body from the <see cref="topNode"/> to the end of the document.
        /// </param>
        /// <returns>The copy of the original <see cref="HtmlDocument"/> with only the requested part.</returns>
        public static HtmlDocument GetPartOfBody(HtmlNode topNode, HtmlNode bottomNode)
        {
            HtmlNode sourceNode = topNode ?? bottomNode;
            if (sourceNode == null)
            {
                throw new ArgumentNullException("bottomNode", "topNode or bottomNode must not be null.");
            }

            if (topNode != null && bottomNode != null && topNode.StreamPosition > bottomNode.StreamPosition)
            {
                throw new InvalidOperationException("topNode must be before bottomNode");
            }

            var htmlDocument = CreateDocument(sourceNode.OwnerDocument.DocumentNode.InnerHtml);
            if (topNode != null)
            {
                topNode = SelectSingleNode(htmlDocument, topNode.XPath);
            }

            if (bottomNode != null)
            {
                bottomNode = SelectSingleNode(htmlDocument, bottomNode.XPath);
            }


            if (topNode != null)
            {
                RemoveAllNodesAbove(topNode);
            }

            if (bottomNode != null)
            {
                RemoveAllNodesBelow(bottomNode);
            }

            return htmlDocument;
        }

        /// <summary>
        /// Gets a claen version of the <see cref="HtmlNode.InnerText"/>.
        /// </summary>
        /// <remarks>
        /// This method cleans all spaces and new lines to be a single space and trims '>' chars from the start (to ignore plain text indentation).
        /// </remarks>
        /// <param name="node">The wanted node.</param>
        /// <returns>The clean inner text.</returns>
        public static string GetCleanInnerText(this HtmlNode node)
        {
            var text = HttpUtility.HtmlDecode(node.InnerText);
            if (text != null)
            {
                text = Regex.Replace(text, @"\s+", " ");
                text = text.Trim().TrimStart('>', ' ');
            }
            return text;
        }

        public static string GetCleanInnerTextWithLineBrakes(this HtmlNode node)
        {
            var text = HttpUtility.HtmlDecode(node.InnerText);
            if (text != null)
            {
                text = text.Trim().TrimStart('>', ' ');
            }
            return text;
        }

        /// <summary>
        /// Removes nodes safely.
        /// If the node has no content it gets removed at whole.
        /// But if there is something in it the child nodes are appended to the parent.
        /// </summary>
        /// <param name="nodes">The nodes to remove.</param>
        public static void RemoveNodesAndKeepContent(IEnumerable<HtmlNode> nodes)
        {
            foreach (var node in nodes)
            {
                if (node.HasChildNodes)
                {
                    string innerText = node.GetCleanInnerText();
                    if (!string.IsNullOrEmpty(innerText) || node.SelectNodes(".//img") != null)
                    {
                        HtmlNode[] children = node.ChildNodes.ToArray();
                        foreach (var child in children)
                        {
                            child.Remove();
                            node.ParentNode.InsertBefore(child, node);
                        }
                    }
                }
                node.Remove();
            }
        }

        /// <summary>
        /// Creates a new <see cref="HtmlDocument"/> and loads the html to it.
        /// </summary>
        /// <param name="html">The html to load.</param>
        /// <returns>A new <see cref="HtmlDocument"/>.</returns>
        public static HtmlDocument CreateDocument(string html)
        {
            var htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }

        /// <summary>
        /// Selects a node that matches the given <see cref="xPath"/>.
        /// This is used because <see cref="HtmlNode.SelectNodes"/> does not work with TextNodes.
        /// </summary>
        /// <param name="document">The document to search in.</param>
        /// <param name="xPath">The xPath to match.</param>
        /// <returns>The first node that matches the <see cref="xPath"/>. If none if found null is returned.</returns>
        private static HtmlNode SelectSingleNode(HtmlDocument document, string xPath)
        {
            return document.DocumentNode.Descendants().FirstOrDefault(node => node.XPath == xPath);
        }

        /// <summary>
        /// Removes all the nodes above the given node.
        /// </summary>
        /// <param name="htmlNode">The node to anchor from.</param>
        /// <param name="includingNode">whether to remove the given node as well.</param>
        private static void RemoveAllNodesAbove(HtmlNode htmlNode, bool includingNode = true)
        {
            HtmlNode anchorNode = htmlNode;

            while (anchorNode != null)
            {
                HtmlNode previousSibling = anchorNode.PreviousSibling;
                while (previousSibling != null)
                {
                    previousSibling.Remove();
                    previousSibling = anchorNode.PreviousSibling;
                }

                if (anchorNode.ParentNode != null && !anchorNode.ParentNode.Name.Equals("body", StringComparison.OrdinalIgnoreCase))
                {
                    anchorNode = anchorNode.ParentNode;
                }
                else
                {
                    anchorNode = null;
                }
            }

            if (includingNode)
            {
                htmlNode.Remove();
            }
        }

        /// <summary>
        /// Removes all the nodes below the given node.
        /// </summary>
        /// <param name="htmlNode">The node to anchor from.</param>
        /// <param name="includingNode">whether to remove the given node as well.</param>
        private static void RemoveAllNodesBelow(HtmlNode htmlNode, bool includingNode = true)
        {
            HtmlNode anchorNode = htmlNode;

            while (anchorNode != null)
            {
                HtmlNode nextSibling = anchorNode.NextSibling;
                while (nextSibling != null)
                {
                    nextSibling.Remove();
                    nextSibling = anchorNode.NextSibling;
                }

                if (anchorNode.ParentNode != null && !anchorNode.ParentNode.Name.Equals("body", StringComparison.OrdinalIgnoreCase))
                {
                    anchorNode = anchorNode.ParentNode;
                }
                else
                {
                    anchorNode = null;
                }
            }

            if (includingNode)
            {
                htmlNode.Remove();
            }
        }

    }
}
